Skip to content

feat(MODEL-MUSIC-MUSIC3): a music-only server, an example that can be heard, and the five keys upstream refuses that we dropped (#672, #953) - #963

Closed
localai-bot wants to merge 8 commits into
mainfrom
row/MUSIC3-PARITY-DOCS-2
Closed

feat(MODEL-MUSIC-MUSIC3): a music-only server, an example that can be heard, and the five keys upstream refuses that we dropped (#672, #953)#963
localai-bot wants to merge 8 commits into
mainfrom
row/MUSIC3-PARITY-DOCS-2

Conversation

@localai-bot

@localai-bot localai-bot commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

feat(MODEL-MUSIC-MUSIC3): a music-only server, an example that can be heard, and the five keys upstream refuses that we dropped (#672, #953)

Developer directive: parity on what upstream supports — "we want to be a good
reference" — usage docs for MiniMax-Music3, and in those docs the weights. Then,
mid-flight: "we should allow to load only the music model" and "we need to have
an e2e test working".

FOLLOWING_AGENTS_PROTOCOL

The upstream surface, enumerated rather than summarized

SGLang-Omni 748a0b43 models/minimax_music3/ and diffusers c6da9936
modular_pipelines/minimax_music3/, read field by field and recorded with
file:line in spec §10.1 so the next reader re-derives nothing.

Closed here: the music-only server, the missing example, and five refusals.
Owed and named: the non-wav response formats, request batching and
/v1/audio/speech/batch, the 32 kHz delivery resample.
Permanently refused rather than owed: streaming — neither upstream arm has
it (supports_streaming_vocoder=False).
One place we are ahead of both arms: guidance_scale is a real per-request
control here, where diffusers freezes it at 1.7 into the guider component
(denoise.py:180) and SGLang exposes it only as a serve-time knob.

--model is optional when --speech-model is given

Serving a 28.5 GB music model also forced loading an unrelated text model, and
on this box the smallest text checkpoint is 35B — so the recipe this project
documented was effectively unrunnable. Upstream's own is sgl-omni serve --model MiniMaxAI/MiniMax-Music3, no text tower anywhere.

vllm-server --speech-model /path/to/minimax-music3

Third instance of a shape already in server_main.cpp: a pooling checkpoint
serves /v1/embeddings alone, a Parakeet checkpoint serves
/v1/audio/transcriptions alone. It mirrors vLLM's task-conditional
registration (api_server.py:255-265).

Additive, and proved rather than argued. The only case whose verdict changes
is BOTH flags absent, which was an error and remains one with a message naming
both options. The route table is gated in both directions over a real socket,
because a handler-dispatch test cannot see route registration at all.

The example the music family did not have

examples/minimax_music3_gen — a thin client of include/vllm.h and nothing
else, like parakeet-transcribe and vllm-cli. Hearing this model previously
needed a running server plus a curl, or a C ABI caller nobody had written.

Five keys upstream refuses by name were SILENT here (#953)

temperature, top_p, top_k, repetition_penalty — refused upstream at
request_builders.py:14-19,109-114, because this model's autoregressive stage
has ONE sampler, a fixed top-50 draw (encoders.py:48,94-103). And
max_new_tokens, upstream's LENGTH spelling in 25 Hz frames rather than seconds
(request_builders.py:56-68), so a 250-frame request silently became the
family's 60 s default. That is the #925 class exactly, in the same file that
already carries #925's refusal one paragraph above. Fixed in flow.

The e2e gate no longer reports a skip wearing a pass

It read 5 cases / 5 passed with assertions: 0 whenever the checkpoint was
absent — the same shape that fooled this project on test_qwen3_paged_engine.
Split into a checkpoint-free half that runs unconditionally in CI (request
contract, both ceilings, the speech-only route table over a real socket with a
stub synthesizer) and the env-gated half, whose HTTP case now drives the real
engine over a real socket against the music-only server shape. A coverage-report
case prints which arms ran, every run.

The full arm was run: POST /v1/audio/speech -> 200 audio/wav, 12332 bytes in 518 s wall, 2 AR frames -> 6 latent frames -> 3072 samples/channel, 6144 int16
samples all non-zero, 0 clipped, 2818 of 3072 positions differing between L and
R, and /v1/completions + /v1/chat/completions both 404 from the route table.
checkpoint_arms_run=5.

arm cases assertions
test_minimax_music3_e2e_real, no env vars 9 37 (was 5 / 0)
test_minimax_music3_e2e_real, checkpoint only 9 86
test_minimax_music3_e2e_real, checkpoint + VLLM_CPP_MUSIC3_DIT=1 9 582
test_speech_api 6 67
test_openai_api_server 62 733
test_openai_conformance 23 252
test_minimax_h3 (unchanged) 79 57395
server flag ctest cases 7 passed

The weights are documented (porting-a-model.md §2.1)

docs/USAGE.md gains component-by-component tables: the diffusers arm at
MiniMaxAI/MiniMax-Music3@fbdf52fbaaca799592917417eb05f1899f1255ec, 28.5 GB
resident
(28 517 617 303 B, measured) out of a 57.4 GB repository and why they
differ; the native .pth arm we refuse and SGLang-Omni serves; the one
implemented GGUF Q4_K artifact with its sha256; and the fourteen third-party
quantized repositories in five formats, each marked refused. The revision is
verified rather than copied — condition_encoder/diffusion_pytorch_model.safetensors
on disk hashes to that revision's own LFS record.

A sample a human can hear

2.0 s of 44100 Hz stereo from this engine in 3286 s of wall clock: RMS 0.03169,
peak 0.97437 with 0 clipped samples, 84 073 of 88 064 positions differing
between left and right. Its samples are compared to nothing — §5 withdrew
the token gate — so it shows the pipeline runs, not that the music is right. It
is not committed: check-pr-size.py classifies every path and none takes a
.wav outside tests/, where a file compared to nothing would sit beside the
goldens and imply it was one.

The four asks, answered directly

1. Music-only server. vllm-server --speech-model <dir> with NO --model
starts and serves, observed live rather than inferred:

server: speech/music-only model (family=minimax-music3, 44100 Hz,
        text-only synthesis, family DETECTED); serving /v1/audio/speech
server: listening on http://0.0.0.0:18923 (model 'minimax-music3')

--model alone and --model + --speech-model are byte-identical in
behaviour
. The whole change is one new early branch, if (args.model_dir.empty()),
which loads the speech engine and return 0s before reaching a single line of
the existing path; nothing downstream of it was touched. The only case whose
verdict changes is BOTH flags absent, which was an error and remains one.
Server suites: 7 of 7 ctest cases (4 pre-existing + 3 new — neither flag is
still an error and now names both options; --speech-model alone reaches the
speech LOAD; --speech-family alone still demands a checkpoint),
test_openai_api_server 62 cases / 733 assertions (+1 case / +24 assertions,
the speech-only route table over a real socket), test_openai_conformance
23 / 252 unchanged.

2. e2e, three arms. What a bare CI run executes unconditionally: the request
contract on the exact body the real case posts, the near-miss and sampling
refusals, the duration arithmetic including both ceilings, and the speech-only
route table over a real socket with a stub synthesizer. What stays env-gated:
everything needing the 28.5 GB checkpoint, plus the two 2.4B-DiT arms behind
VLLM_CPP_MUSIC3_DIT.

arm cases assertions checkpoint arms run
no env vars (what CI runs) 9 37 0 — was 5 cases / 0 assertions
VLLM_CPP_MUSIC3_CHECKPOINT 9 86 3
+ VLLM_CPP_MUSIC3_DIT=1 9 582 5

3. The five keys. All five were accepted and silently dropped; all five
are now refused by name.

key upstream anchor why it cannot be honoured
temperature request_builders.py:14-19,109-114 the AR stage's only sampler is a fixed top-50 draw, encoders.py:48,94-103
top_p same no nucleus branch exists
top_k same _AR_SAMPLING_TOP_K is a module constant of 50
repetition_penalty same no penalty is applied anywhere in the loop
max_new_tokens request_builders.py:56-68, constants.py:4-5 upstream's LENGTH, in 25 Hz frames not seconds; the refusal names audio_duration and the /25 conversion

4. The weights table (docs/USAGE.md, "MiniMax-Music3: the exact weights").
It carries: repo and revision —
MiniMaxAI/MiniMax-Music3@fbdf52fbaaca799592917417eb05f1899f1255ec, verified
rather than copied, since condition_encoder/diffusion_pytorch_model.safetensors
on disk hashes to 83179c5e…a202c2a4d, that revision's own LFS record; the
Q4_K artifact's sha256 4c5d41b2…c70cbdd0 at revision c36aaeed… with its exact
byte count; 28.5 GB resident (28 517 617 303 B, measured) versus 57.4 GB
repository
, with the reason they differ; the refused native .pth arm
(qwen_7B/, flowmatching_vae.pth, dav.pth) and that SGLang-Omni serves it;
and all fourteen community quant repositories across five formats, each marked
refused and each marked third-party rather than first-party. This is the
first application of .agents/porting-a-model.md §2.1 (landing as #951).

One red that belonged to nobody (#965)

windows-msvc-cpu/windows-msvc-vulkan failed here, and they are not #645
(M_PI in three LTX2 sources). They were W6's own
C4456: declaration of 'loaded' hides previous local declaration at
server_main.cpp:1315 — the only warning in the job, on main since W6 landed.
The matched-arm check is what separated it from my diff: #956, #950 and #939, all
touching no speech surface, fail identically. main has no baseline because
windows-msvc-* are PR-only (#584), so it presents to each author in turn as
their own red. Filed and fixed in flow by renaming the inner declaration; nothing
suppressed.

Mutations

Four run, four fire: sampling refusal neutered (5 assertions red),
max_new_tokens refusal neutered (2 red), --model made mandatory again
(2 ctest cases red), generate routes registered unconditionally (3 cases /
6 assertions red in the api-server suite, 1 / 2 in the e2e suite). Sources
restored and verified sha256-identical.

Supersedes #954, which carried two untrailered merge commits; this branch is the
same tree with a linear history.

Issue: #672

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]

mudler added 4 commits August 15, 2026 21:17
… heard, and the five keys upstream refuses that we dropped (#672, #953)

Developer directive: parity on what upstream supports ("we want to be a good
reference"), usage docs, and in those docs the weights — then, mid-flight,
"we should allow to load only the music model" and "we need to have an e2e test
working".

UPSTREAM WAS ENUMERATED FIELD BY FIELD, not summarized: SGLang-Omni `748a0b43`
`models/minimax_music3/` and diffusers `c6da9936`
`modular_pipelines/minimax_music3/`, recorded with `file:line` in spec section
10.1 so the next reader re-derives nothing.

--model IS NOW OPTIONAL WHEN --speech-model IS GIVEN. Serving a 28.5 GB music
model also forced loading an unrelated text model, and on this box the smallest
text checkpoint is 35B — so the recipe this project documented was effectively
unrunnable, while upstream's own is `sgl-omni serve --model
MiniMaxAI/MiniMax-Music3` with no text tower anywhere (`config.py:29-63`: three
stages, no chat LLM). `--speech-model` alone now loads only the speech engine
and registers only `/v1/audio/speech`. It is the THIRD instance of a shape
already in this file — a pooling checkpoint serves `/v1/embeddings` alone, a
Parakeet checkpoint serves `/v1/audio/transcriptions` alone — and it mirrors
vLLM's task-conditional registration (`api_server.py:255-265`).

IT IS ADDITIVE AND THAT IS PROVED, NOT ARGUED. The only case whose verdict
changes is BOTH flags absent, which was an error and remains one, with a message
that now names both ways to satisfy it. `--model` alone and `--model` +
`--speech-model` take byte-identical paths. The route table is gated in BOTH
DIRECTIONS OVER A REAL SOCKET, because a handler-dispatch test cannot see route
registration at all: with no synthesizer `/v1/audio/speech` is a 404 from the
route table with no envelope leaked, and on a speech-only server
`/v1/completions` and `/v1/chat/completions` are 404 while `/v1/audio/speech`
returns audio/wav.

THE MUSIC FAMILY WAS THE ONE GENERATIVE FAMILY IN THIS TREE WITH NO EXAMPLE.
`examples/minimax_music3_gen` is a THIN client of `include/vllm.h` and nothing
else, like `parakeet-transcribe` and `vllm-cli`: `vllm_speech_engine_load`, the
three interrogations of the handle, `vllm_synthesize`, and the RIFF bytes the
result already carries. `--lyrics`/`--description` take `@path` because a
`[Verse]` tag inside an argv is easy to mangle, and it reports the DELIVERED
length rather than the requested one, since a duration resolves to a whole
number of 25 Hz frames.

FIVE KEYS UPSTREAM REFUSES BY NAME WERE SILENT HERE (#953, fixed in flow).
`temperature`, `top_p`, `top_k`, `repetition_penalty` — `request_builders.py:14-19,109-114`
refuses all four, because this model's AR stage has ONE sampler, a fixed top-50
draw (`encoders.py:48,94-103`): there is no temperature to set and no nucleus
branch to widen. And `max_new_tokens`, which is upstream's LENGTH spelling in
25 Hz FRAMES rather than seconds (`request_builders.py:56-68`), so a 250-frame
request silently became the family's 60 s default. That is the #925 class
exactly, in the same file that already carries #925's refusal one paragraph
above.

THE E2E GATE NO LONGER REPORTS A SKIP WEARING A PASS. It read 5 cases /
5 passed with `assertions: 0` whenever the checkpoint was absent — five green
case names over an empty run, the same shape that fooled this project on
`test_qwen3_paged_engine`. The file is split: a CHECKPOINT-FREE half runs
unconditionally in CI (the request contract on the exact body the real case
posts, both ceilings, and the speech-only route table over a real socket with a
stub synthesizer), and the env-gated half now drives the REAL engine over a REAL
SOCKET against the music-only server shape rather than calling
`handle_audio_speech`. A coverage-report case prints which arms ran, every run.
Its assertion is deliberately NOT a cross-case counter — `-tc` runs one case
alone, the counter is legitimately zero, and a gate that reds for the way it was
invoked is a gate somebody deletes; it asserts a cheap fact about the checkpoint
itself instead (44100 Hz, hop 512, vocab 200000, 8 codebooks, read from the
component config.json files).

  no env vars      9 cases /  37 assertions   (was 5 / 0)
  test_speech_api  6 cases /  67 assertions   (was 5 / 47)
  openai_api_server 62 cases / 733 assertions (was 61 / 719)
  server flag cases 7 passed                  (was 4)

THE WEIGHTS ARE DOCUMENTED, per `.agents/porting-a-model.md` section 2.1.
`docs/USAGE.md` carries component-by-component tables for the diffusers arm at
`MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec` — 28.5 GB
resident (28 517 617 303 B, MEASURED) out of a 57.4 GB repository, and why the
two differ — the native `.pth` arm we refuse and SGLang-Omni serves, the one
implemented GGUF Q4_K artifact with its sha256, and the FOURTEEN THIRD-PARTY
quantized repositories in five formats, each marked refused and each marked
third-party. The revision is VERIFIED rather than copied:
`condition_encoder/diffusion_pytorch_model.safetensors` on disk hashes to
`83179c5e…a202c2a4d`, which is that revision's own LFS record for the file.

OWED AND NAMED rather than left to be discovered: the non-`wav` response formats
(no encoder vendored — and note upstream DOWNMIXES TO MONO for every non-wav
format, `client/audio.py:328-334`), request batching and
`/v1/audio/speech/batch` (SGLang runs continuous batching at 16 with TWO engine
rows per request for the CFG twin, `engine_builder.py:74-77`), and the 32 kHz
delivery resample. Streaming is a PERMANENT refusal rather than a debt: neither
upstream arm has it (`supports_streaming_vocoder=False`).

One place we are AHEAD of both arms: `guidance_scale` is a real per-request
control here, where diffusers freezes it at 1.7 into the guider component
(`denoise.py:180`) and SGLang exposes it only as a serve-time knob.

Four mutations run, all four fire: the sampling refusal neutered (5 assertions
red), the `max_new_tokens` refusal neutered (2 red), `--model` made mandatory
again (2 ctest cases red), and the generate routes registered unconditionally
(3 cases / 6 assertions red in the api-server suite and 1 / 2 in the e2e suite).
Sources restored and verified sha256-identical.

Issue: #672

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…t takes RUN_SERIAL like the other two that do (#672)

The speech-only route-table cases added in the previous commit bind a real
cpp-httplib server on an ephemeral port. `test_openai_api_server` and
`test_openai_conformance` already carry `RUN_SERIAL` for exactly that reason,
recorded three lines below in this file: under a saturated `ctest -jN` the
server's accept thread starves and the client sees a connection failure rather
than the defect it was looking for. A socket test that flakes teaches people to
re-run it, which is how a real red gets waved through.

Issue: #672

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…hy it is not committed (#672)

The e2e gate's own artifact is 0.07 s — the shortest request that still enters
every stage — which nobody can listen to. `minimax-music3-gen --duration 2.0
--steps 2 --seed 7` produced 88 064 frames per channel of 44100 Hz stereo in
3286 s of wall clock on a 20-core x86 CPU box at load average 7-150.

Verified by RE-READING the RIFF file rather than trusting the generator's own
report: RMS 0.03169, peak 0.97437 full-scale with 0 CLIPPED samples, 175 858 of
176 128 int16 samples non-zero, and 84 073 of 88 064 positions differing between
left and right — so the 128 latent channels are folded into two streams of 64
rather than interleaved, which is one of the four ways a well-formed WAV can
still not be a song.

ITS SAMPLES ARE COMPARED TO NOTHING, and that is structural rather than an
omission. Spec section 5 withdrew the token gate because upstream's
autoregressive stage has no greedy path, and a request's waveform can never
equal `waveform.npy` in any case: both the codes and the initial latents are
seeded random draws. The clip demonstrates that the pipeline RUNS and emits a
well-formed, non-silent, non-clipped, genuinely stereo signal. It says nothing
about whether the music is right; the per-stage gates are what say that. No
speed claim is made or implied — the acoustic half is upstream's own fp32 and
the depth decoder and DiT are scalar host loops by construction.

IT IS NOT COMMITTED, and the reason is a CHECKER rather than a preference.
`scripts/check-pr-size.py` classifies every repository path: `ASSET` takes
`assets/*.{png,svg}`, `BENCH_EVIDENCE` takes
`benchmarks/{demo,media}/*.{json,png,gif,mp4,log}`, and neither takes a `.wav`.
The only classified home for one is under `tests/`, where a file compared to
nothing would sit beside the oracle goldens and imply it was one — which
`test_minimax_music3_e2e_real.cpp` already refuses for its own artifact in as
many words. Widening either pattern would be widening a checker's scope to make
a change pass, which AGENTS.md forbids without its own spec and red-before
evidence, and one demo clip does not justify one. Regenerating it is a single
command, and that command is now in the docs.

Issue: #672

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…kticks (#672)

The weights table's last component row names the checkpoint set's ROOT — the
three files that sit beside the six component directories. It was written as an
empty inline-code span, which renders as two backticks and a bracketed word
rather than as a name, so the one row a reader needs in order to know the root
carries files at all was the least legible in the table.

Issue: #672

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
mudler added 3 commits August 15, 2026 21:30
…erence between them is visible (#672)

Spec section 10.3 said the suite was split into a checkpoint-free half and an
env-gated half. It did not say what each arm actually examines, which leaves the
reader to take "split" on trust — and the whole point of the split was that a
gate which cannot say HOW MANY things it examined has not reported.

Measured on this box: 9 cases / 37 assertions with no env vars (it was 5 / 0),
9 / 86 with the checkpoint alone, and 9 / 582 with the checkpoint and
VLLM_CPP_MUSIC3_DIT=1. The counters the coverage-report case prints go with
them: 0, 3 and 5 checkpoint arms respectively.

The full arm's own numbers are recorded too, because they are the first ones
taken over a REAL SOCKET against the real 28.5 GB engine rather than a stub:
200 audio/wav, 12332 bytes in 518.0 s of wall clock, 2 AR frames to 6 latent
frames to 3072 samples per channel, 6144 int16 samples all non-zero with 0
clipped and 2818 of 3072 positions differing between left and right — and
/v1/completions and /v1/chat/completions both 404 from the route table, which is
the music-only claim proved over the wire rather than through a handler call.

Issue: #672

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
FOLLOWING_AGENTS_PROTOCOL

Keeps the branch current before review. No content of this row changes; the
incoming commit is the weight-offload docs fix (#958).

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
FOLLOWING_AGENTS_PROTOCOL

Keeps the branch current before review. The incoming commit is the LTX-2.5
token-append phase loop (#930); it touches no MiniMax-Music3 surface, and the
keyed records it edits (issue-index, FEATURES, USAGE) merge on disjoint keys.

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…W6's own variable shadow (#965, #672)

`windows-msvc-cpu` and `windows-msvc-vulkan` fail on every open pull request:

  server_main.cpp(1315,55): error C2220: the following warning is treated as an error
  server_main.cpp(1315,55): warning C4456: declaration of 'loaded' hides
                            previous local declaration

That is W6's own speech-attach block declaring `loaded` inside the scope of the
text engine's `loaded` at `:1025`. It is the ONLY warning in the job, it has
been on `main` since W6 landed, and it is fixed here by renaming the inner
declaration. Nothing is suppressed and no detector is weakened.

WHAT FOUND IT WAS THE MATCHED-ARM CHECK, NOT THE LABEL, and that is the part
worth recording. Both jobs are habitually red and habitually attributed to #645
— which is the `M_PI` portability regression in three LTX2 sources: different
file, different detector, different failure. A second cause sitting behind a
known-red name is invisible for exactly as long as nobody reads the log.

Three unrelated open pull requests that touch no speech surface — #956, #950 and
#939 — fail with the identical C4456, which is what separates "pre-existing"
from "mine". `main` carries no baseline because `windows-msvc-*` are PR-only
(#584), so the failure presents to each author in turn as a red their own diff
caused.

Verified after the rename: 7 of 7 server ctest cases pass, `test_openai_api_server`
is 62 cases / 733 assertions, and `vllm-server --speech-model <dir>` with no
`--model` starts and serves for real:

  server: speech/music-only model (family=minimax-music3, 44100 Hz,
          text-only synthesis, family DETECTED); serving /v1/audio/speech
  server: listening on http://0.0.0.0:18923 (model 'minimax-music3')

Issue: #965

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
@localai-bot

Copy link
Copy Markdown
Collaborator Author

Superseded by #966 — same tree, one extra docs/USAGE.md block.

documentation-checkpoint walks every commit in PR_BASE..PR_HEAD, not the diff as a whole, and 74444f119 (the #965 windows fix) touched src/vllm/entrypoints/openai/server_main.cpp — classified user_usage — without a docs/USAGE.md change in the same commit. A follow-up commit cannot repair that, because the gate re-reads the offending commit standalone.

The right repair was for that commit to have carried its documentation, and it now does: #966 documents what a successful music-only start prints (family DETECTED vs DECLARED, text-only synthesis vs reference clip REQUIRED), which is genuinely what a user needs to tell a working server from a merely listening one.

Verified before pushing this time: check-doc-checkpoint green on all six commits individually AND over origin/main..HEAD, plus check-commit-trailers, check-commit-style, check-agent-record, check-public-doc-tables and check-now-current. Every source file is byte-identical to the one built and gated.

@localai-bot
localai-bot deleted the row/MUSIC3-PARITY-DOCS-2 branch August 16, 2026 00:47
localai-bot added a commit that referenced this pull request Aug 16, 2026
… heard, and the five keys upstream refuses that we dropped (#672, #953, #965) (#966)

feat(MODEL-MUSIC-MUSIC3): a music-only server, an example that can be
heard, and the five keys upstream refuses that we dropped (#672, #953)

Developer directive: parity on what upstream supports — "we want to be a
good
reference" — usage docs for MiniMax-Music3, and in those docs the
weights. Then,
mid-flight: "we should allow to load only the music model" and "we need
to have
an e2e test working".

FOLLOWING_AGENTS_PROTOCOL

## The upstream surface, enumerated rather than summarized

SGLang-Omni `748a0b43` `models/minimax_music3/` and diffusers `c6da9936`
`modular_pipelines/minimax_music3/`, read field by field and recorded
with
`file:line` in spec §10.1 so the next reader re-derives nothing.

**Closed here:** the music-only server, the missing example, and five
refusals.
**Owed and named:** the non-`wav` response formats, request batching and
`/v1/audio/speech/batch`, the 32 kHz delivery resample.
**Permanently refused rather than owed:** streaming — neither upstream
arm has
it (`supports_streaming_vocoder=False`).
**One place we are ahead of both arms:** `guidance_scale` is a real
per-request
control here, where diffusers freezes it at 1.7 into the guider
component
(`denoise.py:180`) and SGLang exposes it only as a serve-time knob.

## `--model` is optional when `--speech-model` is given

Serving a 28.5 GB music model also forced loading an unrelated text
model, and
on this box the smallest text checkpoint is 35B — so the recipe this
project
documented was effectively unrunnable. Upstream's own is `sgl-omni serve
--model
MiniMaxAI/MiniMax-Music3`, no text tower anywhere.

    vllm-server --speech-model /path/to/minimax-music3

Third instance of a shape already in `server_main.cpp`: a pooling
checkpoint
serves `/v1/embeddings` alone, a Parakeet checkpoint serves
`/v1/audio/transcriptions` alone. It mirrors vLLM's task-conditional
registration (`api_server.py:255-265`).

**Additive, and proved rather than argued.** The only case whose verdict
changes
is BOTH flags absent, which was an error and remains one with a message
naming
both options. The route table is gated in both directions over a real
socket,
because a handler-dispatch test cannot see route registration at all.

## The example the music family did not have

`examples/minimax_music3_gen` — a thin client of `include/vllm.h` and
nothing
else, like `parakeet-transcribe` and `vllm-cli`. Hearing this model
previously
needed a running server plus a `curl`, or a C ABI caller nobody had
written.

## Five keys upstream refuses by name were SILENT here (#953)

`temperature`, `top_p`, `top_k`, `repetition_penalty` — refused upstream
at
`request_builders.py:14-19,109-114`, because this model's autoregressive
stage
has ONE sampler, a fixed top-50 draw (`encoders.py:48,94-103`). And
`max_new_tokens`, upstream's LENGTH spelling in 25 Hz frames rather than
seconds
(`request_builders.py:56-68`), so a 250-frame request silently became
the
family's 60 s default. That is the #925 class exactly, in the same file
that
already carries #925's refusal one paragraph above. Fixed in flow.

## The e2e gate no longer reports a skip wearing a pass

It read 5 cases / 5 passed with **`assertions: 0`** whenever the
checkpoint was
absent — the same shape that fooled this project on
`test_qwen3_paged_engine`.
Split into a checkpoint-free half that runs unconditionally in CI
(request
contract, both ceilings, the speech-only route table over a real socket
with a
stub synthesizer) and the env-gated half, whose HTTP case now drives the
real
engine over a real socket against the music-only server shape. A
coverage-report
case prints which arms ran, every run.

The full arm was run: `POST /v1/audio/speech -> 200 audio/wav, 12332
bytes in
518 s wall`, 2 AR frames -> 6 latent frames -> 3072 samples/channel,
6144 int16
samples all non-zero, 0 clipped, 2818 of 3072 positions differing
between L and
R, and `/v1/completions` + `/v1/chat/completions` both 404 from the
route table.
`checkpoint_arms_run=5`.

| arm | cases | assertions |
|---|---|---|
| `test_minimax_music3_e2e_real`, no env vars | 9 | 37 (was 5 / **0**) |
| `test_minimax_music3_e2e_real`, checkpoint only | 9 | 86 |
| `test_minimax_music3_e2e_real`, checkpoint + `VLLM_CPP_MUSIC3_DIT=1` |
9 | **582** |
| `test_speech_api` | 6 | 67 |
| `test_openai_api_server` | 62 | 733 |
| `test_openai_conformance` | 23 | 252 |
| `test_minimax_h3` (unchanged) | 79 | 57395 |
| server flag ctest cases | 7 passed | |

## The weights are documented (porting-a-model.md §2.1)

`docs/USAGE.md` gains component-by-component tables: the diffusers arm
at
`MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec`,
**28.5 GB
resident** (28 517 617 303 B, measured) out of a 57.4 GB repository and
why they
differ; the native `.pth` arm we refuse and SGLang-Omni serves; the one
implemented GGUF Q4_K artifact with its sha256; and the fourteen
third-party
quantized repositories in five formats, each marked refused. The
revision is
verified rather than copied —
`condition_encoder/diffusion_pytorch_model.safetensors`
on disk hashes to that revision's own LFS record.

## A sample a human can hear

2.0 s of 44100 Hz stereo from this engine in 3286 s of wall clock: RMS
0.03169,
peak 0.97437 with 0 clipped samples, 84 073 of 88 064 positions
differing
between left and right. **Its samples are compared to nothing** — §5
withdrew
the token gate — so it shows the pipeline runs, not that the music is
right. It
is not committed: `check-pr-size.py` classifies every path and none
takes a
`.wav` outside `tests/`, where a file compared to nothing would sit
beside the
goldens and imply it was one.

## The four asks, answered directly

**1. Music-only server.** `vllm-server --speech-model <dir>` with NO
`--model`
starts and serves, observed live rather than inferred:

    server: speech/music-only model (family=minimax-music3, 44100 Hz,
text-only synthesis, family DETECTED); serving /v1/audio/speech
    server: listening on http://0.0.0.0:18923 (model 'minimax-music3')

`--model` alone and `--model` + `--speech-model` are **byte-identical in
behaviour**. The whole change is one new early branch, `if
(args.model_dir.empty())`,
which loads the speech engine and `return 0`s before reaching a single
line of
the existing path; nothing downstream of it was touched. The only case
whose
verdict changes is BOTH flags absent, which was an error and remains
one.
Server suites: **7 of 7** ctest cases (4 pre-existing + 3 new — neither
flag is
still an error and now names both options; `--speech-model` alone
reaches the
speech LOAD; `--speech-family` alone still demands a checkpoint),
`test_openai_api_server` **62 cases / 733 assertions** (+1 case / +24
assertions,
the speech-only route table over a real socket),
`test_openai_conformance`
**23 / 252** unchanged.

**2. e2e, three arms.** What a bare CI run executes unconditionally: the
request
contract on the exact body the real case posts, the near-miss and
sampling
refusals, the duration arithmetic including both ceilings, and the
speech-only
route table over a real socket with a stub synthesizer. What stays
env-gated:
everything needing the 28.5 GB checkpoint, plus the two 2.4B-DiT arms
behind
`VLLM_CPP_MUSIC3_DIT`.

| arm | cases | assertions | checkpoint arms run |
|---|---|---|---|
| no env vars (what CI runs) | 9 | **37** | 0 — was 5 cases / **0
assertions** |
| `VLLM_CPP_MUSIC3_CHECKPOINT` | 9 | **86** | 3 |
| + `VLLM_CPP_MUSIC3_DIT=1` | 9 | **582** | 5 |

**3. The five keys.** All five were **accepted and silently dropped**;
all five
are **now refused by name**.

| key | upstream anchor | why it cannot be honoured |
|---|---|---|
| `temperature` | `request_builders.py:14-19,109-114` | the AR stage's
only sampler is a fixed top-50 draw, `encoders.py:48,94-103` |
| `top_p` | same | no nucleus branch exists |
| `top_k` | same | `_AR_SAMPLING_TOP_K` is a module constant of 50 |
| `repetition_penalty` | same | no penalty is applied anywhere in the
loop |
| `max_new_tokens` | `request_builders.py:56-68`, `constants.py:4-5` |
upstream's LENGTH, in 25 Hz frames not seconds; the refusal names
`audio_duration` and the /25 conversion |

**4. The weights table** (`docs/USAGE.md`, "MiniMax-Music3: the exact
weights").
It carries: repo **and** revision —
`MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec`,
verified
rather than copied, since
`condition_encoder/diffusion_pytorch_model.safetensors`
on disk hashes to `83179c5e…a202c2a4d`, that revision's own LFS record;
the
Q4_K artifact's sha256 `4c5d41b2…c70cbdd0` at revision `c36aaeed…` with
its exact
byte count; **28.5 GB resident (28 517 617 303 B, measured) versus 57.4
GB
repository**, with the reason they differ; the refused native `.pth` arm
(`qwen_7B/`, `flowmatching_vae.pth`, `dav.pth`) and that SGLang-Omni
serves it;
and all fourteen community quant repositories across five formats, each
marked
refused and each marked **third-party** rather than first-party. This is
the
first application of `.agents/porting-a-model.md` §2.1 (landing as
#951).

## Two reds stacked behind one habitually-red job name

`windows-msvc-cpu`/`windows-msvc-vulkan` are usually attributed to #645
(`M_PI`
in three LTX2 sources). **Neither of the two causes here was #645**, and
the
first hid the second.

**#965, fixed in flow.** `C4456: declaration of 'loaded' hides previous
local
declaration` at `server_main.cpp:1315` — W6's own speech-attach block
declaring
`loaded` inside the text engine's `loaded`. The only warning in the job,
on
`main` since W6 landed. Matched arm: #956, #950 and #939, none touching
the
speech surface, fail identically. Renamed; nothing suppressed.

**#968, filed and NOT fixed here.** With the shadow gone the same jobs
failed
again on `C4244: conversion from 'const double' to 'float'`, raised
inside
MSVC's `<vector>` from `ltx2_video.cpp:203,214` — two narrowing
`positions.assign` calls that `c7cb59fbb` (#964) landed on `main` while
this row
was in flight. **This branch touches zero LTX2 files.** The matched arm
splits
exactly on the merge base: #966 and #951 (on `c7cb59fbb`) hit it,
#967/#956/
#950/#939/#938 (before it) do not. It is deliberately left to the
LTX-2.5 lane —
#964's own comment reasons that "double -> float -> double reproduces
the bits",
so a silencing cast is a claim about that reasoning rather than a
formatting fix.

**The finding, which outlives both:** a known-red list tells you a job
is often
red. It never tells you that today's red is the same one. Only reading
the log
does — and here it took two readings, because removing the first cause
is what
made the second visible.

<!-- kept for the record -->
### The first of the two, in detail (#965)

`windows-msvc-cpu`/`windows-msvc-vulkan` failed here, and they are
**not** #645
(`M_PI` in three LTX2 sources). They were W6's own
`C4456: declaration of 'loaded' hides previous local declaration` at
`server_main.cpp:1315` — the only warning in the job, on `main` since W6
landed.
The matched-arm check is what separated it from my diff: #956, #950 and
#939, all
touching no speech surface, fail identically. `main` has no baseline
because
`windows-msvc-*` are PR-only (#584), so it presents to each author in
turn as
their own red. Filed and fixed in flow by renaming the inner
declaration; nothing
suppressed.

## Mutations

Four run, four fire: sampling refusal neutered (5 assertions red),
`max_new_tokens` refusal neutered (2 red), `--model` made mandatory
again
(2 ctest cases red), generate routes registered unconditionally (3 cases
/
6 assertions red in the api-server suite, 1 / 2 in the e2e suite).
Sources
restored and verified sha256-identical.

Supersedes #954 (untrailered merge commits) and #963 (a
`server_main.cpp` commit
that owed `docs/USAGE.md` under the per-commit documentation
checkpoint). Same
tree, linear history, every commit green on `check-doc-checkpoint`,
`check-commit-trailers` and `check-commit-style` locally before pushing.
Every
source file is byte-identical to the one built and gated.

Issue: #672

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants